﻿using System;
using System.Collections.Generic;
//using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Text.RegularExpressions;

namespace thesebas.memcache
{
    public class MemcacheException : Exception
    {
        public MemcacheException() : base() { }
        public MemcacheException(string message) : base(message) { }
        public MemcacheException(string message, Exception innerException) : base(message, innerException) { }



    }
    public class Version : IComparable
    {
        private string version;
        public Version(string version)
        {
            this.version = version;
        }

        #region IComparable Members

        public int CompareTo(object obj)
        {

            string[] tv = this.version.Split('.');
            string[] ov = ((Version)obj).version.Split('.');
            if (tv.Length != ov.Length) return tv.Length - ov.Length;

            for (int i = 0; i < tv.Length; i++)
            {
                int t, o;
                int.TryParse(tv[i], out t);
                int.TryParse(ov[i], out o);
                if (t != o) return t - o;
            }
            return 0;
        }

        #endregion

        public override string ToString()
        {
            return this.version;
        }


    }

    public class StatsMalloc : Stats
    {

        //stats malloc
        //STAT arena_size 82862080
        //STAT free_chunks 50
        //STAT fastbin_blocks 0
        //STAT mmapped_regions 169
        //STAT mmapped_space 180236288
        //STAT max_total_alloc 0
        //STAT fastbin_space 0
        //STAT total_alloc 79438448
        //STAT total_free 3423632
        //STAT releasable_space 2361472    

        public int ArenaSize { get { return (int)this.values["arena_size"]; } }
        public int FreeChunks { get { return (int)this.values["free_chunks"]; } }
        public int FastbinBlocks { get { return (int)this.values["fastbin_blocks"]; } }
        public int MMappedRegions { get { return (int)this.values["mmapped_regions"]; } }
        public int MMappedSpace { get { return (int)this.values["mmapped_space"]; } }
        public int MaxTotalAlloc { get { return (int)this.values["max_total_alloc"]; } }
        public int FastbinSpace { get { return (int)this.values["fastbin_space"]; } }
        public int TotalAlloc { get { return (int)this.values["total_alloc"]; } }
        public int TotalFree { get { return (int)this.values["total_free"]; } }
        public int ReleasableSpace { get { return (int)this.values["releasable_space"]; } }
        public void Parse(string line)
        {

            Match m = Regex.Match(line, @"STAT (?<field>[a-z_]+) (?<value>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (m.Success)
            {
                switch (m.Groups["field"].Value)
                {
                    case "arena_size":
                    case "free_chunks":
                    case "fastbin_blocks":
                    case "mmapped_regions":
                    case "mmapped_space":
                    case "max_total_alloc":
                    case "fastbin_space":
                    case "total_alloc":
                    case "total_free":
                    case "releasable_space":

                        int ivalue;
                        int.TryParse(m.Groups["value"].Value, out ivalue);
                        this.values[m.Groups["field"].Value] = ivalue;
                        break;
                }
            }
        }
        public override string ToString()
        {
            StringBuilder s = new StringBuilder();
            s.AppendLine("#stats malloc");
            foreach (KeyValuePair<string, object> item in this.values)
            {
                s.AppendFormat("{0,-25}: {1}\n", item.Key, item.Value);
            }
            return s.ToString();
        }
    }

    public class Stats
    {
        protected Dictionary<string, Object> values;
        public Stats()
        {
            this.values = new Dictionary<string, object>();
        }
    }
    public class StatsBasic : Stats
    {
        public DateTime UpTimeSince { get { return this.Time.AddSeconds(-this.UpTime); } }

        public Int64 PID { get { return (Int64)this.values["pid"]; } }
        public Int64 UpTime { get { return (Int64)this.values["uptime"]; } }
        public DateTime Time { get { return (DateTime)this.values["time"]; } }
        public Version Version { get { return (Version)this.values["version"]; } }
        public Int64 PointerSize { get { return (Int64)this.values["pointer_size"]; } }
        public Int64 CurrentItems { get { return (Int64)this.values["curr_items"]; } }
        public Int64 TotalItems { get { return (Int64)this.values["total_items"]; } }
        public Int64 Bytes { get { return (Int64)this.values["bytes"]; } }
        public Int64 CurrentConnections { get { return (Int64)this.values["curr_connections"]; } }
        public Int64 TotalConnection { get { return (Int64)this.values["total_connections"]; } }
        public Int64 ConnectionStructures { get { return (Int64)this.values["connection_structures"]; } }
        public Int64 GetCommands { get { return (Int64)this.values["cmd_get"]; } }
        public Int64 SetCommands { get { return (Int64)this.values["cmd_set"]; } }
        public Int64 GetHits { get { return (Int64)this.values["get_hits"]; } }
        public Int64 GetMissed { get { return (Int64)this.values["get_misses"]; } }
        public Int64 BytesRead { get { return (Int64)this.values["bytes_read"]; } }
        public Int64 BytesWritten { get { return (Int64)this.values["bytes_written"]; } }
        public Int64 LimitMaxBytes { get { return (Int64)this.values["limit_maxbytes"]; } }

        public float RUsageUser { get { return (float)this.values["rusage_user"]; } }
        public float RUsageSystem { get { return (float)this.values["rusage_system"]; } }
        public Int64 Evictions { get { return (Int64)this.values["evictions"]; } }
        public Int64 Threads { get { return (Int64)this.values["pid"]; } }


        //dynamic stats

        public float GetsPerSecond { get { return this.GetCommands / this.UpTime; } }
        public float SetsPerSecond { get { return this.SetCommands / this.UpTime; } }
        public float HitsPerSecond { get { return this.GetHits / this.UpTime; } }
        public float MissesPerSecond { get { return this.GetMissed / this.UpTime; } }
        public float BytesReadPerSecond { get { return this.BytesRead / this.UpTime; } }
        public float BytesWrittenPerSecond { get { return this.BytesWritten / this.UpTime; } }
        public float EvictionsPerSecond { get { return this.Evictions / this.UpTime; } }
        public float HitsAccuracy { get { return (float)this.GetHits / this.GetCommands; } }
        public float Usage { get { return (float)this.Bytes / this.LimitMaxBytes; } }

        public void Parse(string line)
        {
            Match m = Regex.Match(line, @"STAT (?<field>[a-z_]+) (?<value>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (m.Success)
            {
                switch (m.Groups["field"].Value)
                {
                    // stats
                    //STAT pid 3891
                    //STAT uptime 2083815
                    //STAT time 1243784019
                    //STAT version 1.2.6
                    //STAT pointer_size 64
                    //STAT rusage_user 276.267001
                    //STAT rusage_system 1020.074925
                    //STAT curr_items 610458
                    //STAT total_items 9939012
                    //STAT bytes 469916419
                    //STAT curr_connections 26
                    //STAT total_connections 2752173
                    //STAT connection_structures 92
                    //STAT cmd_get 50574009
                    //STAT cmd_set 9987833
                    //STAT get_hits 27459584
                    //STAT get_misses 23114425
                    //STAT evictions 2206
                    //STAT bytes_read 12786796486
                    //STAT bytes_written 32241233931
                    //STAT limit_maxbytes 536870912
                    //STAT threads 8



                    //ints
                    case "pid":
                    case "uptime":
                    case "pointer_size":
                    case "curr_items":
                    case "total_items":
                    case "bytes":
                    case "curr_connections":
                    case "total_connections":
                    case "connection_structures":
                    case "cmd_get":
                    case "cmd_set":
                    case "get_hits":
                    case "get_misses":
                    case "bytes_read":
                    case "bytes_written":
                    case "limit_maxbytes":
                    case "evictions":
                    case "threads":

                        Int64 ivalue;
                        Int64.TryParse(m.Groups["value"].Value, out ivalue);
                        this.values[m.Groups["field"].Value] = ivalue;
                        break;



                    //timestamps
                    case "time":
                        int time;
                        if (int.TryParse(m.Groups["value"].Value, out time))
                        {
                            this.values[m.Groups["field"].Value] = new DateTime(1970, 1, 1).AddSeconds(time);
                        }

                        break;
                    //strings
                    case "version":

                        this.values["version"] = new Version(m.Groups["value"].Value);
                        break;

                    //floats
                    case "rusage_user":
                    case "rusage_system":
                        float fvalue;
                        float.TryParse(m.Groups["value"].Value, out fvalue);
                        this.values[m.Groups["field"].Value] = fvalue;
                        break;
                };
            }
        }

        public Dictionary<string, Object> ToDictionary()
        {
            Dictionary<string, Object> d = new Dictionary<string, object>();
            return d;
        }
        public override string ToString()
        {
            StringBuilder s = new StringBuilder();
            s.AppendLine("#stats");
            foreach (KeyValuePair<string, object> item in this.values)
            {
                s.AppendFormat("{0,-25}: {1}\n", item.Key, item.Value);
            }
            return s.ToString();
        }
        public StatsDynamicBasic StatsDynamic(StatsBasic other)
        {
            return new StatsDynamicBasic(this, other);
        }
    }

    public class StatsDynamicBasic
    {
        StatsBasic older, newer;
        public StatsDynamicBasic(StatsBasic a, StatsBasic b)
        {
            if (a.Time > b.Time)
            {
                older = b;
                newer = a;
            }
            else
            {
                older = a;
                newer = b;
            }

        }

        private Int64 timediff { get { return this.newer.UpTime - this.older.UpTime; } }

        public StatsBasic Newer { get { return this.newer; } }
        public float Gets { get { return (float)(this.newer.GetCommands - this.older.GetCommands) / this.timediff; } }
        public float Sets { get { return (float)(this.newer.SetCommands - this.older.SetCommands) / this.timediff; } }
        public float Hits { get { return (float)(this.newer.GetHits - this.older.GetHits) / this.timediff; } }
        public float Misses { get { return (float)(this.newer.GetMissed - this.older.GetMissed) / this.timediff; } }
        public float BytesRead { get { return (float)(this.newer.BytesRead - this.older.BytesRead) / this.timediff; } }
        public float BytesWritten { get { return (float)(this.newer.BytesWritten - this.older.BytesWritten) / this.timediff; } }
        public float Evictions { get { return (float)(this.newer.Evictions - this.older.Evictions) / this.timediff; } }

    }


    public class StatsSlabs : Stats
    {
        Dictionary<int, StatsSlabs.Slab> slabs = new Dictionary<int, Slab>();
        public class Slab : Stats
        {
            //STAT 29:chunk_size 61600
            //STAT 29:chunks_per_page 17
            //STAT 29:total_pages 1
            //STAT 29:total_chunks 17
            //STAT 29:used_chunks 17
            //STAT 29:free_chunks 0
            //STAT 29:free_chunks_end 16
            public int ChunkSize { get { return (int)this.values["chunk_size"]; } }
            public int ChunksPerPage { get { return (int)this.values["chunks_per_page"]; } }
            public int TotalPages { get { return (int)this.values["total_pages"]; } }
            public int TtotalChunks { get { return (int)this.values["total_chunks"]; } }
            public int UsedChunks { get { return (int)this.values["used_chunks"]; } }
            public int FreeChunks { get { return (int)this.values["free_chunks"]; } }
            public int FreeChunksEnd { get { return (int)this.values["free_chunks_end"]; } }

            public int SlabNumber { get { return (int)this.slabNo; } }

            int slabNo;
            internal void setProperty(string name, object value)
            {
                this.values[name] = value;
            }
            public Slab(int no)
            {
                this.slabNo = no;
            }
        }
        public void Parse(string line)
        {

            Match m;
            //STAT 29:chunk_size 61600
            //STAT 29:chunks_per_page 17
            //STAT 29:total_pages 1
            //STAT 29:total_chunks 17
            //STAT 29:used_chunks 17
            //STAT 29:free_chunks 0
            //STAT 29:free_chunks_end 16

            m = Regex.Match(line, @"STAT (?<slabno>\d)\:(?<field>[a-z_]+) (?<value>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (m.Success)
            {
                try
                {
                    int slabno;
                    slabno = int.Parse(m.Groups["slabno"].Value);
                    int v;
                    if (!int.TryParse(m.Groups["value"].Value, out v)) v = -1;
                    this.values[m.Groups["field"].Value] = v;

                    if (!this.slabs.ContainsKey(slabno))
                    {
                        this.slabs[slabno] = new StatsSlabs.Slab(slabno);
                    }
                    this.slabs[slabno].setProperty(m.Groups["field"].Value, v);


                }
                catch (Exception e) { }
                return;
            }
            //STAT active_slabs 29
            //STAT total_malloced 83712904

            m = Regex.Match(line, @"STAT (?<field>[a-z_]+) (?<value>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (m.Success)
            {
                switch (m.Groups["field"].Value)
                {
                    case "active_slabs":
                    case "total_malloced":
                        int v;
                        if (!int.TryParse(m.Groups["value"].Value, out v)) v = -1;
                        this.values[m.Groups["field"].Value] = v;
                        break;
                }
                return;
            }

        }
    }
    public enum StoreCommand { Set, Add, Replace, Append, Prepend, CheckAndSet }//"set", "add", "replace", "append" or "prepend"
    public enum StoreResponse { Stored, NotStored, NotFound, Exists }//
    public class MemcacheClient
    {
        IPEndPoint endpoint;
        TcpClient tclient;
        NetworkStream nstream;
        StreamWriter swriter;
        StreamReader sreader;
        Boolean Connected
        {
            get { return this.tclient.Connected; }
        }

        Version version;
        public Version Version { get { if (this.version == null) this.version = this.GetVersion(); return this.version; } }

        public MemcacheClient()
        {

        }
        public void connect(TcpClient tcpclient)
        {
            this.tclient = tcpclient;
            if (!this.tclient.Connected)
            {
                if (this.endpoint == null) throw new MemcacheException("not connected");
                this.tclient.Connect(this.endpoint);
            }
            this.nstream = this.tclient.GetStream();
            this.sreader = new StreamReader(this.nstream);
            this.swriter = new StreamWriter(this.nstream);
            this.swriter.AutoFlush = true;
        }

        public void connect(IPEndPoint endpoint)
        {
            TcpClient cl = new TcpClient();
            cl.Connect(endpoint);
            this.connect(cl);
        }


        public void connect(String host, int port)
        {
            IPAddress ip;

            try
            {
                ip = IPAddress.Parse(host);
            }
            catch (Exception)
            {
                try
                {
                    IPHostEntry entry = Dns.GetHostEntry(host);
                    ip = entry.AddressList[0];
                }
                catch (Exception)
                {
                    throw new MemcacheException(String.Format("wrong host name [{0}]", host));
                }
            }
            this.connect(new IPEndPoint(ip, port));
        }
        public void connect(IPAddress ipaddress, int port)
        {
            this.connect(new IPEndPoint(ipaddress, port));
        }



        public StatsBasic BasicStats()
        {
            if (!this.Connected) throw new MemcacheException("not connected!");
            this.swriter.WriteLine("stats");
            string response = this.sreader.ReadLine();
            StatsBasic stats = new StatsBasic();
            if (response == "ERROR") throw new MemcacheException("stats error (not supported stats??");

            while (response != "END")
            {
                stats.Parse(response);
                response = this.sreader.ReadLine();

            } while (response != "END") ;


            return stats;
        }

        public StatsMalloc MallocStats()
        {
            if (!this.Connected) throw new MemcacheException("not connected!");

            string response = "";
            StatsMalloc stats = new StatsMalloc();
            this.swriter.WriteLine("stats malloc");
            response = this.sreader.ReadLine();
            if (response == "ERROR") throw new MemcacheException("'stats malloc' not supported");

            while (response != "END")
            {
                stats.Parse(response);
                response = this.sreader.ReadLine();
            }
            return stats;
        }
        protected Version GetVersion()
        {
            if (!this.Connected) throw new MemcacheException("not connected!");
            this.swriter.WriteLine("version");
            string response = this.sreader.ReadLine();
            Match m = Regex.Match(response, "VERSION (?<version>.+)");
            if (m.Success)
            {
                return new Version(m.Groups["version"].Value);
            }
            throw new MemcacheException("error executing 'version' command");

        }

        public void FlushAll() { this.FlushAll(0); }
        public void FlushAll(int timeout)
        {
            if (!this.Connected) throw new MemcacheException("not connected!");
            this.swriter.WriteLine("flush_all");
            string response = this.sreader.ReadLine();

            if (response != "OK") throw new MemcacheException("error executing 'flush_all' command");
        }

        public StoreResponse Set(MemcacheItem item, int exptime)
        {
            if (!this.Connected) throw new MemcacheException("not connected!");
            return this.setcmd(StoreCommand.Set, item.Key, item.Flags, exptime, 0, item.data, false);
        }

        public StoreResponse Add(MemcacheItem item, int exptime)
        {
            if (!this.Connected) throw new MemcacheException("not connected!");
            return this.setcmd(StoreCommand.Add, item.Key, item.Flags, exptime, 0, item.data, false);
        }

        public StoreResponse Append(MemcacheItem item)
        {
            if (!this.Connected) throw new MemcacheException("not connected!");
            if (this.Version.CompareTo(new Version("1.2.4")) < 0) throw new MemcacheException(String.Format("append supported since 1.2.4, current version {0}", this.Version));
            return this.setcmd(StoreCommand.Append, item.Key, 0, 0, 0, item.data, false);
        }
        public StoreResponse Prepend(MemcacheItem item)
        {
            if (!this.Connected) throw new MemcacheException("not connected!");
            if (this.Version.CompareTo(new Version("1.2.4")) < 0) throw new MemcacheException(String.Format("append supported since 1.2.4, current version {0}", this.Version));
            return this.setcmd(StoreCommand.Prepend, item.Key, 0, 0, 0, item.data, false);
        }
        public StoreResponse Replace(MemcacheItem item, int exptime)
        {
            if (!this.Connected) throw new MemcacheException("not connected!");
            return this.setcmd(StoreCommand.Replace, item.Key, item.Flags, exptime, 0, item.data, false);
        }
        public StoreResponse CheckAndSet(MemcacheItem item, int exptime)
        {
            if (!this.Connected) throw new MemcacheException("not connected!");
            return this.setcmd(StoreCommand.CheckAndSet, item.Key, item.Flags, exptime, 0, item.data, false);
        }
        protected StoreResponse setcmd(StoreCommand command, string key, Int32 flags, int exptime, Int64 unique, byte[] data, bool noreply)
        {
            //<command name> <key> <flags> <exptime> <bytes> [noreply]\r\n
            //cas <key> <flags> <exptime> <bytes> <cas unqiue> [noreply]\r\n

            object[] par;

            switch (command)
            {
                default:
                case StoreCommand.Set:
                case StoreCommand.Add:
                case StoreCommand.Replace:
                case StoreCommand.Append:
                case StoreCommand.Prepend:
                    par = new object[] { key, flags, exptime, data.Length };
                    break;
                case StoreCommand.CheckAndSet:
                    par = new object[] { key, flags, exptime, data.Length, unique };
                    break;
            }
            string cmdstring;
            switch (command)
            {
                default:
                case StoreCommand.Set: cmdstring = String.Format("set {0} {1} {2} {3}", par); break;
                case StoreCommand.Add: cmdstring = String.Format("add {0} {1} {2} {3}", par); break;
                case StoreCommand.Replace: cmdstring = String.Format("replace {0} {1} {2} {3}", par); break;
                case StoreCommand.Append: cmdstring = String.Format("append {0} {1} {2} {3}", par); break;
                case StoreCommand.Prepend: cmdstring = String.Format("prepend {0} {1} {2} {3}", par); break;
                case StoreCommand.CheckAndSet: cmdstring = String.Format("cas {0} {1} {2} {3} {4}", par); break;
            }

            //Console.WriteLine(cmdstring);
            this.swriter.WriteLine(cmdstring);
            string resp;
            if (this.nstream.DataAvailable)
            {
                resp = this.sreader.ReadLine();
                if (resp == "ERROR") throw new MemcacheException("error putting item to mc");
            }

            this.nstream.Write(data, 0, data.Length);
            this.swriter.WriteLine();
            resp = this.sreader.ReadLine();
            switch (resp)
            {
                case "STORED": return StoreResponse.Stored;
                case "NOT_STORED": return StoreResponse.NotStored;
                case "EXISTS": return StoreResponse.Exists;
                case "NOT_FOUND": return StoreResponse.NotFound;

            }
            throw new MemcacheException("unexpected store response");
            //return SetResponse.Stored;
        }
        public MemcacheItem[] Get(string[] keys)
        {
            return this.Get(String.Join(" ", keys), false);
        }
        public MemcacheItem[] Get(string[] keys, bool withCas)
        {
            return this.Get(String.Join(" ", keys), withCas);

        }
        public MemcacheItem[] Get(string key)
        {
            return this.Get(key, false);
        }
        public MemcacheItem[] Get(string key, bool withCas)
        {
            if (!this.Connected) throw new MemcacheException("not connected!");
            string cmd = String.Format((withCas ? "gets {0}" : "get {0}"), key);
            this.swriter.WriteLine(cmd);
            string resp = this.sreader.ReadLine();
            if (resp == "ERROR") throw new MemcacheException("error getting items");
            List<MemcacheItem> items = new List<MemcacheItem>();
            while (resp != "END")
            {
                Match m = Regex.Match(resp, @"VALUE (?<key>[^\s]+)\s+(?<flags>\d+)\s+(?<bytes>\d+)(\s+(?<unique>\d+))?");//VALUE <key> <flags> <bytes> [<cas unique>]

                if (!m.Success) throw new MemcacheException("failed to parse item header");
                MemcacheItem item = new MemcacheItem();
                item.Key = m.Groups["key"].Value;
                int bytes;
                if (!int.TryParse(m.Groups["bytes"].Value, out bytes)) throw new MemcacheException("error parsing item size");
                if (!int.TryParse(m.Groups["flags"].Value, out item.Flags)) throw new MemcacheException("error parsing item flags");
                if (m.Groups["unique"].Success)
                {
                    if (!Int64.TryParse(m.Groups["flags"].Value, out item.Unique)) throw new MemcacheException("error parsing item flags");
                }
                item.data = new byte[bytes];

                //this.sreader.BaseStream.Read(item.data, 0, item.Bytes);
                //this.sreader.BaseStream.ReadByte();
                //this.sreader.BaseStream.ReadByte();
                string data = this.sreader.ReadLine();
                items.Add(item);

                resp = this.sreader.ReadLine();

            }
            return items.ToArray();
        }



    }


    public class MemcacheItem
    {
        public string Key;
        public Int32 Flags;
        public Int64 Unique;

        public byte[] data;
        //protected 

        public override string ToString()
        {
            return String.Format("key: {0}, flags:{1}, bytes: {2}, uniq:{3}",
                this.Key, this.Flags, this.data.Length, this.Unique
                );

        }
    }
}
